Lattices and Single Value Abstractions
Back to main -- Previous chapter: While language -- Next chapter: Interval analysis on the While language
A non-relational abstract interpreter approximates program behavior by tracking properties of individual variables independently, without encoding correlations between them. It stores facts like x \in V where V is some subset of \mathbb Z. This is in contrast to relational abstractions, like polyhedra and octagon, which capture dependencies between variables (i.e. store facts like x + y \leqslant c).
In this chapter, we will discuss these core building blocks of the Codex library:
Latticesprovide the algebra: how abstract values are ordered, joined, intersected, and widened.Single_value_abstraction(SVA): build on top of lattices to give the semantics of operators (arithmetic, boolean, bitwise, etc.) behave in the abstract world.
Lattices
A lattice (wikipedia) is the backbone of abstract domains: it defines how to combine information (meet), compare precision (lattice order), and ensure loops converge (widening). The module Lattices.Sig defines signatures for different kinds of lattices. Within Codex, one can find all the lattice implementations in Lattices module.
The core lattice operations are:
includes: t -> t -> boolor\sqsubseteq- check if one value is more precise than another. Smaller values are more precise (they concretize to a smaller set). Note thatincludes a bisb \sqsubseteq a.join: t -> t -> tor\sqcup- merges information from two paths (e.g., after anif, merge both branches).inter: t -> t -> t(meet) or\sqcap- refines information by keeping only what both values allow.widen: t -> t -> tor\nabla- ensures termination in loops by accelerating convergence when values keep growing. Overapproximates join.
In the following, we discuss two complete lattices. One which abstracts booleans, called quadrivalent, and one which abstracts integers, called interval.
Quadrivalent Lattice
The quadrivalent lattice is a finite domain for booleans with four elements. It is the powerset of \{\text{true};\text{false}\}. Its elements are named Bottom (\emptyset, infeasible), True, False, and Top (\{\text{true};\text{false}\}, unknown). Its definitions of join (\sqcup), and inter (\sqcap) are simply set union/intersection. Since the lattice is finite, we do not need widening for termination.
module QuadrivalentLattice = struct
type t = Bottom | True | False | Top
let join a b = match (a,b) with
| Bottom, x | x, Bottom -> x
| False, False -> False
| True, True -> True
| _ -> Top
let inter a b = match a,b with
| Bottom, _ | _, Bottom -> Bottom
| Top, x | x, Top -> x
| True, False | False, True -> Bottom
| True, True -> True | False, False -> False
let includes a b = match a,b with
| Top, _ -> true
| _, Bottom -> true
| True, True -> true
| False, False -> true
| _ -> false
let widen ~previous b = join previous b
endCodex has a ready-made quadrivalent lattice implementation in Lattices.Quadrivalent.
Interval Lattice
An interval is represented as a pair of values of type Integer_Or_Inf.t. Each bound can either be negative infinity, positive infinity, or a finite integer. This allows intervals to describe both finite ranges and unbounded ranges in a uniform way.
First, we define the module Integer_Or_Inf, which defined operations on our extended integer type \mathbb{Z} \cup \{+\infty, -\infty\}:
module Integer_Or_Inf = struct
type t =
| Ninf (* Negative infinity *)
| Pinf (* Positive infinity . *)
| Finite of Z.t
let min a b = match a,b with
| Ninf, _ | _, Ninf -> Ninf
| x, Pinf | Pinf, x -> x
| Finite a, Finite b -> Finite(min a b)
let max a b = match a,b with
| Pinf, _ | _, Pinf -> Pinf
| x, Ninf | Ninf, x -> x
| Finite a, Finite b -> Finite(max a b)
let add a b = match a,b with
| Ninf, Pinf | Pinf, Ninf -> assert false
| Ninf, _ | _, Ninf -> Ninf
| Pinf, _ | _, Pinf -> Pinf
| Finite a, Finite b -> Finite(Z.add a b)
let neg = function
| Ninf -> Pinf
| Pinf -> Ninf
| Finite a -> Finite(Z.neg a)
let pp fmt = function
| Ninf -> Format.fprintf fmt "-∞"
| Pinf -> Format.fprintf fmt "+∞"
| Finite a -> Z.pp_print fmt a
let equal a b = match a, b with
| Ninf, Ninf | Pinf, Pinf -> true
| Finite a, Finite b -> Z.equal a b
| _ -> false
let leq a b = equal (min a b) a
endUsing this type, we can define our interval lattice in the IntervalLattice module:
module IntervalLattice = struct
(* Constraints: the min cannot be Pinf , the max cannot be Ninf. *)
type t = Integer_Or_Inf.t * Integer_Or_Inf.t
let pp fmt (min,max) = Format.fprintf fmt "[%a,%a]" Integer_Or_Inf.pp min Integer_Or_Inf.pp max
let equal (mina,maxa) (minb,maxb) =
Integer_Or_Inf.equal mina minb && (Integer_Or_Inf.equal maxa maxb)
let top = (Integer_Or_Inf.Ninf, Integer_Or_Inf.Pinf)
let singleton a = let ext = Integer_Or_Inf.Finite a in (ext,ext)
let join (mina,maxa) (minb,maxb) =
(Integer_Or_Inf.min mina minb, Integer_Or_Inf.max maxa maxb)
let inter (mina,maxa) (minb,maxb) =
(Integer_Or_Inf.max mina minb, Integer_Or_Inf.min maxa maxb)
let includes a b = equal (inter a b) b
endIn practice, Codex reuses Frama-C’s built-in interval library Ival, which offers a richer representation than this simple pair. In Ival, intervals are part of a more general type Top that can also capture congruence and modular information in addition to lower and upper bounds. There is also a Set constructor, so small sets of values can be stored exactly.
type t = private
| Set of Int.t array
| Float of Fval.t
(** [Top(min, max, rem, modulo)] represents the interval between
[min] and [max], congruent to [rem] modulo [modulo]. A value of
[None] for [min] (resp. [max]) represents -infinity
(resp. +infinity). [modulo] is > 0, and [0 <= rem < modulo].
Actual [Top] is thus represented by Top(None,None,Int.zero,Int.one) *)
| Top of Int.t option * Int.t option * Int.t * Int.tThis richer representation allows Codex to handle numeric domains more precisely than basic intervals.
Single Value Abstractions
Conceptually, a Single-Value Abstraction (SVA) is an abstract domain for numeric values (integers, booleans, bitvectors...). Its elements are thus abstractions of sets of these values. An SVA packages:
- A lattice implementation, as seen in the previous section, which defines how to order, join, meet, and widen elements.
Transfer functions: these are the abstract operations, they over-approximate concrete program operations. In the case of SVA, these operations are arithmetic (
+,-,*), bitwise (&,|,^, ...) and comparison (==,<=, ...) operators on values.We distinguish between forward transfer function, which compute the results of an operation, and backward transfer functions, which refine inputs from known results.
- forward transfer functions: Given abstract values for the inputs, compute the abstract value of the output. Example: from intervals
[0,5]and[2,3],iaddgives[2,8]. backward transfer functions: Given the output and some information about the input, refine the possible values of the other input. Example: if
x + y ∈ [10,10]andy ∈ [2,3], then we can refinexto[7,8].In Codex, these return an option type for each argument, where
Noneis used to indicate no new information was learned. So in our example,iadd top [2,3] [10,10]would returnSome [7,8], Nonesince no information is learned ony.
- forward transfer functions: Given abstract values for the inputs, compute the abstract value of the output. Example: from intervals
This separation allows Lattices to supply the algebra, while SVAs define the semantics of operators (arithmetic, comparisons, bitwise, etc.).
The Single_value_abstraction library contains various implementations of SVAs within Codex. We provide simplified example implementations below.
Quadrivalent Abstraction
As the quadrivalent lattice exactly represents \mathcal{P}(\{\text{true}, \text{false}\}), we can easily implement the best forward transfer functions as follows, which are sound and exact.
module SVA_Quadrivalent = struct
include QuadrivalentLattice
(** Forward transfer functions *)
module Boolean_Forward = struct
(** Boolean not operator *)
let neg = function
| Bottom -> Bottom | True -> False | False -> True | Top -> Top
(** Boolean and operator *)
let band a b = match a,b with
| Bottom, _ | _, Bottom -> Bottom
| False, _ | _, False -> False
| True,True -> True
| Top,True | True, Top | Top,Top -> Top
(** Boolean or operator *)
let bor a b = match a,b with
| Bottom, _ | _, Bottom -> Bottom
| True, _ | _, True -> True
| False,False -> False
| Top,False | False, Top | Top,Top -> Top
end
(** backward transfer functions *)
module Boolean_Backward = struct
let neg x result =
let notres = Boolean_Forward.neg result in (* neg is involutive, so we can reuse it here *)
let refined_x = inter x notres in
if includes refined_x x then None else Some refined_x
let band a b result = match result with
| Top -> (None, None)
| True -> (* a && b is true only when both are true *)
((if includes True a then None else Some True), (if includes True b then None else Some True))
| False -> (* we only know that one of them has to be false *)
begin match a, b with
| Top, True -> (Some False, None)
| True, Top -> (None, Some False)
| _ -> (None, None)
end
| Bottom -> (None, None)
(* bor is similar to band ... *)
end
endIn Codex, the full implementation of is in Single_value_abstraction.Quadrivalent.
Interval Single-Value-Abstraction
To reason about programs abstractly, we need transfer functions that describe how arithmetic and boolean operations behave over intervals. Instead of showing the full implementation, we present a simplified Interval_Forward template.
The template illustrates abstract versions of Add, Neg, Sub, as well as boolean comparisons \leq and =. For booleans, results are returned using the quadrivalent lattice. The complete Integer_Forward implementation is available in Codex under Single_value_abstraction.Ival.
module SVA_Interval = struct
include IntervalLattice
module Integer_Forward = struct
open Integer_Or_Inf
let add (mina,maxa) (minb,maxb) = (add mina minb, add maxa maxb)
let neg (min,max) = (neg max, neg min)
let sub itva itvb = add itva (neg itvb)
let leq (mina,maxa) (minb,maxb) = (* a <= b *)
let open QuadrivalentLattice in
match maxa,minb with
| Finite maxa, Finite minb when Z.leq maxa minb -> True
| _ -> begin
match mina,maxb with
| Finite mina, Finite maxb when Z.gt mina maxb -> False
| _ -> Top
end
let eq (mina,maxa) (minb,maxb) =
let open QuadrivalentLattice in
match mina,maxa,minb,maxb with
| _, Finite maxa, Finite minb, _ when Z.lt maxa minb -> False
| Finite mina, _, _, Finite maxb when Z.gt mina maxb -> False
| Finite mina, Finite maxa, Finite minb, Finite maxb when
Z.equal mina maxa && (Z.equal mina minb) && (Z.equal mina maxb) -> True
| _ -> Top
end
module Integer_Backward = struct
let add a b result =
let refined_a = inter a (Integer_Forward.sub result b) in
let refined_b = inter b (Integer_Forward.sub result a) in
(if includes refined_a a then None else Some refined_a),
(if includes refined_b b then None else Some refined_b)
(* ... *)
end
endBack to main -- Previous chapter: While language -- Next chapter: Interval analysis on the While language